Rooms and Regions
In addition to Thing, the other absolutely essential class you need to write a TADS game with the adv3Lite library is Room. Every game must have at least one Room in which the action takes place. Your game may have several Rooms (depending on the size of the game world you want to implement). Note that in Interactive Fiction in general and the adv3Lite in particular a Room isn't necessarily a room in a house (such as a kitchen or study) but any area the contents of which are considered accessible to the player character; so, while a Room could be a conventional room in a house, it could also be one corner of a city square, or a section of a riverbank, or a woodland path, or a meadow, part of the deck of a ship, or any number of other such things.
A Room is a kind of Thing (or, to put it more technically, Room is a subclass of Thing) so Room inherits all the properties and methods of Thing, but in practice you won't use many of them, and in the main you'll be using the methods and properties specific to Room. (Possible exceptions include the vocab property and the isLit property, which determines if the Room is lit or dark; by default it's lit. Another clear exception is the desc property, which contains the description of the Room).
Defining a Room
The basic properties to define on a Room are its roomTitle (the name that's displayed in bold at the start of a room description), its desc (the body of the room description) and, optionally, its vocab. The normal way to define these properties is through the Room template. Without the vocab property this looks like this:
kitchen: Room 'Kitchen'
"This kitchen is equipped much as you'd expect, with, for example, a sink
over by the window, a large table in the middle of the room, and an oven
over by the back door to the east, not far from the fridge. The other exits
are west to the hall, north to the dining-room and down to the cellar. "
;
If the vocab property is defined, it is given in a second single-quoted string, thus:
kitchen: Room 'Kitchen' 'kitchen'
"This kitchen is equipped much as you'd expect, with, for example, a sink
over by the window, a large table in the middle of the room, and an oven
over by the back door to the east, not far from the fridge. The other exits
are west to the hall, north to the dining-room and down to the cellar. "
;
There are a couple of advantages to defining the vocab property on a Room:
- If you want the player to be able to use the GO TO command (implemented via pathfind.t) rooms have to have vocab words for the player to be able to refer to them in a command like GO TO KITCHEN.
- By defining the vocab property you also automatically define the Room's name property (and hence it's theName property), which can be useful if you want the game to display a message that includes the name of the Room (e.g. 'You wander into the kitchen', perhaps generated from "You wander into <<getOutermostRoom.theName>>. "). In the kitchen example above this may look a little redundant since the name is the same as the roomTitle, but this need not always be the case. For example you might have a room whose roomTitle is 'Portland Square (east side)', which wouldn't work too well as a name (you might want to name it 'east side of Portland Square', for example).
That said, in many cases you can leave the vocab property to be defined implicitly on many rooms, since the default behaviour of the English-language-specific part of the library is to derive the vocab property from the roomTitle property according to the following rules:
- The vocab property won't be derived from the roomTitle if the vocab property has already been explicitly defined.
- The vocab property won't be derived from the roomTitle is the Room's autoName property is set to nil (it's true by default).
- The vocab property will be a lower-case version of the roomTitle (e.g. 'Kitchen' will become 'kitchen') unless the Room's proper property is set to true (indicating that the Room has a proper name like 'Market Street').
This saves the need to type a name like 'kitchen' twice in the common case, while allowing the vocab property to be somewhat different from the roomTitle property in cases like the Portland Square (east side) example.
By default the roomTitle is displayed in bold at the start of a room description. This normally works well, but if for any reason you want to change this format you can do so by modifying the roomnameStyleTag. For example, if you wanted the roomTitle (the name of the room) to be displayed in bold italics each time, you could override roomnameStyleTag thus:
modify roomnameStyleTag
openText = '\n<b><i>'
closeText = '</i></b>\n'
;
You could also use <FONT> tags here for other effects, for example specifying a particular colour, but this needs to be done with caution, since a colour that looks good in your interpreter may not work so well for players who have chosen a different colour scheme, such as white text on a blue background. On the other hand, there's no reason why you shouldn't use a <FONT> tag to change the size of the room name if you so wished, for example:
modify roomnameStyleTag
openText = '\n<b><FONT SIZE=+2>'
closeText = '</FONT></b>\n'
;
Incidentally, there's also a roomdescStyleTag that can be used in a similar way to format the long description of a room (but not the listing of its contents), a roomcontentsStyleTag that can be used to format the display of a room's contents, and a statusroomStyleTag for formatting the room name as it is displayed in the status line, as well as a number of other StyleTags you can look up in the Library Reference Manual or in the discussion of style tags in the Output and Input Issues chapter later in this manual.
Direction Properties
Unless your game only has a single room, you will generally need to provide some means of travelling from one location to another, and the normal way of doing that in an IF game is by defining direction properties on a Room. These correspond to the command a player would type to move in the corresponding direction (e.g. if the player typed NORTH or GO NORTH the game would attempt to move the player character according to the value of the current room's north property). The adv3Lite library provides 16 such properties:
- The eight compass directions: north, south, east, west, northeast, northwest, southeast and southwest.
- The four shipboard directions: port, starboard, fore and aft.
- The four other directions: up, down, in and out.
You do not have to define all of these directions on every Room (indeed, you'll probably never do so); if any direction property is left at nil that simply means that travel is not possible in that direction. But if you do define any of these properties they can be defined as one of:
- Another Room, in which case the player character would be moved to that room.
- A Door, in which case the player would attempt to go through that door (but may be prevented from doing so if the door is locked).
- A TravelConnector (or any object subclassed from TravelConnector), in which case that TravelConnector's travelVia(actor) method will be triggered.
- A single- or double-quoted string, in which case the string will simply be displayed. The difference is that a double-quoted string will imply travel has been attempted but failed (so beforeTravel notifications will be called, which may cause side-effects such as ending a conversation) whereas a single-quoted string will imply travel has not even been attempted (so that no before travel notifications will be triggered).
- A method, in which case the method will be executed. This can be defined as expression or a method that evaluates to a Room (or other TravelConnector), in which case travel will be attempted to or via that Room/TravelConnector, but in anything but the simplest cases it may be safer to obtain the same effect by defining a TravelConnector with a variable destination.
- The asExit macro, e.g. out asExit(west), in which case the command OUT will result in trying to move the player character west, without OUT being listed as a separate exit in any list of exits.
- The asExitListed macro, e.g. out asExitListed(west), in which case the command OUT will result in trying to move the player character west, but OUT will be listed as a separate exit in any list of exits (this may be useful if, for example, you define a TravelConnector object on one direction property and want to use it on another as well).
- The ulExit(dest) or ulMsgExit(dest, msg) macro. In each of these, where dest is the destination we want the exit to lead to. In the second, msg is the travelDesc we want to be displayed when travel in that direction takes place. Both these macros cause the corresponding exit to be excluded from any listing of exits (the 'ul' is short for 'unlisted').
- The tcMsg(dest, msg) macro, where dest is the destination we want the exit to lead to and msg is the travelDesc we want to be displayed when travel in that direction takes place. With this macro, the exit will be listed in any exit listing.
- Note that all of these macros are used without any punctuation between them and the direction property, e.g.
north tcMsg(garden, "You walk briskly up the path. "). Note also that the msg argument should use double quote marks.
If you define a direction property as a Room, Door, TravelConnector or method, then the corresponding direction will be shown in the list of exits (assuming that the module exits.t is present in your game). You can change this behaviour on a TravelConnector (and hence also on a Room or Door) by setting its isConnectorListed property to nil. A method will (virtually) always be listed as a possible exit (since presumably the point of defining it as a method is that something happens if the player character attempts to move in that direction). A direction on which a double-quoted string has been defined will be shown in the exit lister whereas a direction on which a single-quoted string has been defined will not. This can be changed by overriding the values of exitList.showDQSExits and exitList.showSQSExits respectively.
It follows from this that defining a direction property as a string is equivalent to using a NoTravelMessage in the adv3 library (for which reason NoTravelMsg is not defined in the adv3Lite library). Likewise, a method that simply displays a string is equivalent to an adv3 FakeConnector (which is likewise not defined in the adv3Lite library). For example:
kitchen: Room 'Kitchen' 'kitchen'
"This kitchen is equipped much as you'd expect, with, for example, a sink
over by the window, a large table in the middle of the room, and an oven
over by the back door to the east, not far from the fridge. The other exits
are west to the hall, north to the dining-room and down to the cellar. "
north = diningRoom
east = backDoor
west = hall
down = cellar
south = "Unfortunately, the window appears to be locked, so you can't get out that way. "
southeast = 'There\'s nothing that way but a corner. '
southwest { "You have no reason to visit the pantry. "; }
;
This illustrates what is probably likely to be the most common use of a method defined on a direction property, but in principle such a method can do anything you like, including killing the player character, ending the game in victory, or moving the player character to another location. If a direction-property method does result in the player character being moved to another location, the library keeps track of it in a LookupTable for use by the pathfinder, however for anything other than displaying a message that doesn't result in travel or ending the game, it's probably better and cleaner to use a TravelConnector to carry out the side-effects of travel (rather than defining them in a method). The possibility of doing whatever you like in a method is nevertheless there if you want it.
Note, however, that there is potential gotcha with defining an exit with a method. If the player attempts travel via a direction defined with a method, the method is simply executed; its return value isn't used. This means you can't use a method to return, say, a room or TravelConnector the player character is to travel to or via dependendent on some condition. The same applies to defining an expression on an exit, since this is effectively just a shorthand way of defining a method. It follows that writing code like the following will not work as intended:
kitchen: Room 'Kitchen' 'kitchen'
"This kitchen is equipped much as you'd expect, with, for example, a sink
over by the window, a large table in the middle of the room, and an oven
over by the back door to the east, not far from the fridge. The other exits
are west to the hall, north to the dining-room and down to the cellar. "
/* Don't do this! */
southeast = secretPassage.hasBeenDiscovered ? secretPassage : nil
;
In a situation like this you should instead either start by definining isConnectorApparent = nil on the secretPassage connector and executing secretPassage.isConnectorApparent = true; when the secretPassage is discovered (with kitchen.southeast set to secretPassage throughout), or else have kitchen.southeast initially defined as nil and then change it to secretPassage when the passage is discovered.
The shipboard directions port, starboard, fore and aft will generally be rather meaningless except for rooms that are meant to be aboard a vessel of some sort. Conversely, it's possible, though by no means certain, that a game may want to prevent the use of compass directions when the location is meant to be aboard a ship. To make it easier to deal with such situations Room defines the two properties/methods allowShipboardDirections and allowCompassDirections. If either of these is nil, the corresponding set of directions is disabled for all relevant commands (e.g. GO PORT, THROW BALL PORT or PUSH TROLLEY PORT) carried out in the room in question. Attempts to use the disallowed directions in any command will then be blocked with message stating "Shipboard/compass directions have no meaning here." By default allowCompassDirections is true for all rooms, but game code can override this on rooms that are meant to be aboard ship (or, indeed, in any game that wants to abolish compass directions altogether). On the other hand the default behaviour of allowShipboardDirections() is to return true if and only if one or more of the shipboard direction exits (port, starboard, fore or aft) is non-nil on the room in question. Normally, this means that allowShipboardDirections() can be left to take care of itself, but occasionally you may have rooms, such as a the hold of the ship, where none of the shipboard directions is actually defined (because the only way out is UP, say) but the shipboard directions are still notionally meaningful; on such a room you could simply define allowShipboardDirections = true. (The enforcement of these conditions is carried out by the Doer method checkDirection() which is called from Doer.exec() for any command involving a direction).
Finally, note that defining anything on a direction property of a room establishes a connection in one direction only. For example, in the sample code shown above, defining north = diningRoom on the kitchen establishes a connection to the dining from the kitchen going north, but it doesn't also establish a connection south from the dining room to the kitchen; that would need to be defined explicitly on the diningRoom object. This may be an advantage (a) because it may help to make your code clearer and (b) you may not always want a connection back in the reverse direction, or you may want it to behave differently. On the other hand, if you would prefer the reverse connections to be automatically set up for you, you could try using the symconn extension, which does just that.
Directions
A direction in the adv3Lite object is generally represented by an object of type Direction, usually named with the name of the direction plus 'Dir', e.g. northDir, eastDir, downDir, southwestDir. For the most part you won't need to worry about direction objects since they generally take care of themselves, but occasionally you may want to refer to the name of a direction object when it's used as a parameter for some method or an element of a list (e.g. in the route finder) to indicate the direction taken.
Otherwise, the only time you might want to worry about Direction objects is if you want to define a custom direction. This is a relatively straightforward process, best explained by means of an example. Suppose, for example, you wanted to implement a nornoreast direction which caused the player character to travel via the nornoreast property of the current room when the player entered the command NORNOREAST or NNE. You would just need to do this:
nornoreastDir: CompassDirection
name = 'nornoreast'
dirProp = &nornoreast
sortingOrder = 1450
opposite = sousouwestDir //assuming you were also defining a sousouwest direction
departureName = 'to the nornoreast'
arrivalName = 'from the nornoreast'
;
grammar directionName(nornoreast): 'nornoreast' | 'nne' : Production
dir = nornoreastDir
;
With these two definitions, you could then used nornoreast just like any of the built-in directions. The sortingOrder property on the Direction object defines the order in which this direction will appear in any list of exits; I chose 1450 here since this would make nornoreast come just after northeast. The grammar declaration that follows enables the parser to recognize 'nornoreast' and 'nne' as referring to the nornoreast direction.
Non-Directional Travel Properties
It is also possible to define non-directional travel properties on a Room that work almost exactly like the directional ones except that they do not involve any particular direction, such as jump or that old magic word xyzzy. For example the inside of the little house at the start of Colossal Adventure could be defined like this:
insideBuilding: IndoorRoom 'Inside the Building'
"{I} {am} inside the building, a well house for a large spring. "
out tcMsg(atEndOfRoad, "{I} step{s/?ed} outside. ")
west asExit(out)
xyzzy ulMsgExit(inDebrisRoom, "{I am} translated in the twinkling of an eye. ")
plugh = atY2
;
For this to work, we also need to define actions that will trigger travel via the two magic directions xyzzy and plugh. This can be done succinctly using the appropriate macro:
DefSpecialTravel(Xyzzy, &xyzzy, 'xyzzy') darkTravelAllowed = true;
DefSpecialTravel(Plugh, &plugh, 'plugh') darkTravelAllowed = true;
The macro DefSpecialTravel(name, prop, gram) sets up a VerbRule() with the grammar tag name using gram as its command input grammar, followed by an action called name which will invoke whatever's defined on the current Room's prop. Since this action's definition is unterminated until we add the final semicolon we can add any further properties or methods we need to the definition of our new action. In this example we define darkTravelAllowed = true true on both of them on the principle that the absence of light shouldn't affect the use of a magic word.
DefSpecialTravel(Xyzzy, &xyzzy, 'xyzzy') darkTravelAllowed = true; expands to:
VerbRule(Xyxxy)
'xyzzy'
:VerbProduction
action = Xyzzy
;
Xyzzy: SpecialTravelAction
travelProp = &xyzzy
darkTravelAllowed = true
;
This may often be all that we need, but the SpecialTravelAction class allows further customization through a number of methods and properties, all of which (apart from travelProp, which is already defined by the macro) can be added between the macro invocation and the terminating semicolon:
- travelProp: A property pointer to the Room property (e.g. &jump or &xyzzy) we want this SpecialTravelAction to invoke. This is the one property that user code MUST define, but we'll normally define it through the macro.
- darkTravelAllowed: Flag - do we want to allow this action to trigger travel in the dark? This is nil by default but can be overriden to true for actions (such as magic words) where the lighting may be irrelevant.
- requireOutOfNested: Flag - should travel via this action first remove the actor from any nested room they're in? The default is
true. - fallback(loc): Method to fall back on when this action is invoked in a Room that does not define the corresponding property, or where it is defined as
nil. By default we just display ournoGoodHereMsg. - noGoodHereMsg: The message to display when this action is invoked in a Room that does not define the corresponding property, or where it is defined as
nil(provided we haven't overriddenfallBack(loc)to do something different). By default we just display "That doesn't work here" but user code may often want to override this (orfallBack(loc)to something more appopriate (as the library does for both 'jump' and 'climb'). - cannotTravelInDark(loc): Defines what to so if travel via this action is attempted in a dark room when
darkTravelAllowedis nil. By default we callloc.cannotGoThatWayInDark(travelProp), which by default simply displays loc'scannotGoThatWayInDarkMsg. - noteRetval(loc, val). This is only called if we've defined our non-directional travel property to be a method and its return value is not a TravelConnector, in which case val is the method's return value and
locis the room where the method was invoked. By default we simply displayvalif it's a single-quoted string (which is probably the most useful case) and do nothing otherwise, but game code can override this method to so something different if desired. (If the method does return a TravelConnector, then travel will be attempted via that connector).
Although non-directional travel properties work very like the directional ones (not least insofar as they can be defined in exactly the same way), there are three key differences that may make them the better choice in certain cases:
- Travel via a non-directional property using a non-directional verb does not require the definition of an additional direction (for example, we don't need to define an Xyzzy Direction or Jump direction when neither is truly direction-like). Although in individual rooms we might, for example, define
climb asExit(up), there is no essential connection between any non-directional property and any conventional direction. - Non-directional properties are never listed in any exit lister (seeing 'jump' or 'xyzzy' in a list of exits would propably be jarring).
- The pathfinder ignores non-directional properties.
Finally, (or penultimately), like ordinary travel properties, non-directional travel properties are not confined to triggering travel; we can use them for anything we like, for example:
lowCave: Room 'low cave'
"There is very little headroom here. "
jump = "Ouch! You just banged your head on the ceiling! "
climb = "There's nowhere to climb to here. "
...
;
Finally, finally, the library defines two non-travel actions responding to CLIMB and JUMP (as in the example immediately above). Where no climb or jump property is defined, these commands behave just as they would have done before: "You jump on the spot fruitlessly" or "What do you want to climb?" (in the latter case, the parser may automagically select a climbable object if it's the best bet).
Leveraging Non-Directional Properties for Passages, Paths, and the Like
It may be that we're writing a game that frequently mention passages and/or paths and/or other such things leading off from various locations. These passages, paths and whatever may not be particularly important items in themselves, but it seems appropriate to mention them in our room descriptions. A potential problem is then that players may try to interact with these paths and passages (X PATH, GO THROUGH PASSAGE) and may find it disconcerting if they find our game claiming ignorance of them, but that having to implement a whole lot of PathPassage and Passage objects just to avoid this may feel like over-engineered overkill. Using non-directional travel properties can help here
The first step is to define the relevant actions:
DefSpecialTravel(PassageAction, &passage, 'passage');
DefSpecialTravel(Path, &path, 'path');
(Note that since Passage is the name of an existing class, we have to use a different name for our custom action). Then we can code Room objects like:
hall: Room 'Hall'
"This large hall has a front door leading out to the south and a passage leading
north, with..."
south = frontDoor
north = kitchen
passage = kitchen
out asExit(south)
...
;
frontDoor: DSDoor 'front door' @hall @drive
;
drive: Room 'Drive'
"The front door of the big house lies directly to the north, while a narrow path
leads round the side of the house to the east..."
north = frontDoor
in asExit(north)
east = garden
path = garden
....
;
This will allow players to move by typing PATH or PASSAGE but what we really want is for them to get sensible responses to GO THROUGH PASSAGE or X PASSAGE or FOLLOW PATH. We can do that by defining some suitable MultiLoc Decorations:
MultiLoc, Decoration 'passage; narrow wide'
"Passages are just passages. "
notImportantMsg = 'You can type PASSAGE to go through it. '
initialLocationClass = Room
isInitiallyIn(obj) { return obj.propDefined(&passage); }
dobjFor(Enter)
{
verify() {}
action() { doInstead(PassageAction); }
}
dobjFor(GoThrough) asDobjFor(Enter)
dobjFor(GoAlong) asDobjFor(Enter)
decorationActions = [Examine, GoThrough, GoAlong, Enter]
;
MultiLoc, Decoration 'path; narrow wide broad winding'
"Paths are just paths. "
notImportantMsg = 'You can type PATH to go along it. '
initialLocationClass = Room
isInitiallyIn(obj) { return obj.propDefined(&path); }
dobjFor(Enter)
{
verify() {}
action() { doInstead(Path); }
}
dobjFor(GoThrough) asDobjFor(Enter)
dobjFor(GoAlong) asDobjFor(Enter)
dobjFor(Follow) asDobjFor(Enter)
dobjFor(ClimbDown) asDobjFor(Enter)
dobjFor(ClimbUp) asDobjFor(Enter)
decorationActions = [Examine, GoThrough, GoAlong, Follow, ClimbDown, ClimbUp, Enter]
;
This will place the 'passage' decoration in every room that defines a passage property and the 'path' decoration
in every room that defines a path property. It's fine to have multiple paths or passages leading from the same location
provided we define the corresponding property accordingly, e.g.,
passage = "There are several passages leading from here. You'll have to say
which way you want to go. "
This should then handle commands involving paths and passages quite gracefully, without the need to define a whole load of Passage and PathPassage objects. If we prefer, we can use the built-in ProxyExit class to help with the definition of this kind of object, for example:
ProxyExit 'passage; narrow wide'
"Passages are just passages. "
exitProp = &passage
travelAction = PassageAction
notImportantMsg = 'You can just type PASSAGE to go through it. '
dobjFor(GoAlong) asDobjFor(TravelVia)
dobjFor(Follow) asDobjFor(TravelVia)
dobjFor(ClimbDown) asDobjFor(TravelVia)
dobjFor(ClimbUp) asDobjFor(TravelVia)
decorationActions = inherited + [Follow, GoAlong, ClimbDown, ClimbUp]
;
If we do this we must define the exitProp and travelAction properties (which refer to the room direction property and
SpecialTravelAction we want associated with this ProxyExit), and it's advisabke to define the notImportantMsg to give a suitable response and expand the list of decorationActions to handle other phrasings players might use, such as (in this example), FOLLOW PASSAGE, GO ALONG PASSAGE, GO DOWN PASSSAGE and GO UP PASSAGE.
Alternatively, you can use the proxyExits extension, which includes predefined ProxyExits for doors, passages, paths and archways, along with the corresponding SpecialTravelActions.
Other Room Properties and Methods
Some other properties and methods of Room you may find useful include:
- darkName (single-quoted string) The name to use in place of the roomTitle when the room is dark. By default this is simply 'In the dark', but you can change it to anything you like.
- darkDesc (double-quoted string) The description to use in place of desc when the room is dark. By default this is "It is dark; you can't see a thing." but you can change it to anything you like.
- destName The name of this Room as it will be shown in an explicit exit listing. This defaults to the Room's theName.
- roomFirstDesc (double-quoted string) If this is defined, it will be displayed the first time the room is described (the desc property being used thereafter).
- isIlluminated() and litWithin() Both these methods return true if the room is lit and nil if it's in darkness (a room is lit if its own isLit property is true or if there's a visible light source within the room)
- cannotGoThatWayMsg (double-quoted string) The message that's displayed if travel is attempted in a direction that's undefined (i.e. nil). By default this is "You can't go that way. "
- cannotGoThatWay(dir) The method that's called when travel is attempted in an undefined direction. By default this simply displays the cannotGoThatWayMsg and then displays a list of available exits, if the exits.t module is present. The dir parameter is the direction object corresponding to the attempted direction of travel, e.g. northDir.
- allowDarkTravel (true or nil) Normally (when this property is set to nil) we don't allow travel from one dark location to another. Set this property to true if you do want to allow travel from this location when dark to another dark location.
- cannotGoThatWayInDarkMsg (double-quoted string) The message to display when travel is not allowed from a dark location (either because the direction doesn't lead anywhere or because it goes to another dark location). By default this is "It's too dark to see where you're going. "
- cannotGoThatWayInDark(dir) The method that's called when travel is disallowed from a dark location. The default behaviour is to display the cannotGoThatWayInDarkMsg and then display a list of available exits (if the exits.t module is present). Note that in this context the available exits will be only those that lead to illuminated locations (unless allowDarkTravel is true).
- roomBeforeAction() This method is called on the room just before an action is about to take place, allowing the room to respond to or block the incipient action.
- roomAfterAction() This method allows the room to respond to an action that's just taken place (e.g. by reporting an echo if the action was YELL or saying that the player character just cracked his head on the low ceiling if the action was JUMP)
- roomDaemon() This method is called on the player character's location (the room s/he's in) each turn towards the end of the action processing cycle. It can be used, for example, to display a series of atmpospheric message string (by defining the Room as also being a EventList in which case roomDaemon() would automatically call its doScript() method to cycle through its eventList, unless roomDaemon had been overridden to do something different). If the eventList contains a list of atmospheric strings including sounds (as might often be the case) there's could be a clash with the response to LISTEN and an atmospheric message displayed on the same turn. This can be suppressed by setting the Room's noScriptAfterListen property to true (the default) to prevent roomDaemon() calling doScript() on the same turn as a LISTEN command. If the Room's eventList contains nothing but atmospheric sounds and there is nothing else in scope to responde to a LISTEN command, you may also want to define the Room's listenDesc property as listenDesc() { { doScript();} }.
- extraScopeItems A list of items that would not normally be in scope but which should nevertheless be placed in scope in this room. This can of course be defined as a method that returns different lists of items under different circumstances.
- regions An optional list of the regions that this room is regarded as being within (for the concept of regions, see below). Note that rooms can also be associated with Regions via the Regions' roomList property.
- isIn(region) Tests whether this Room is in the specified region. Note that this isn't simply a matter of testing whether the specified region is listed in the room's regions property, since one region may be inside another. Thus, for example, if the regions property of the hall was [downstairs] and the regions property of the downstairs region was [indoors], hall.isIn(indoors) would be true.
- visited (true or nil) Has the player character visited (i.e. been in) this room? This is true either if a room description has been shown when the room is lit, or if it's been shown when recognizableInDark is true for this room. Note that the room's examined property is also set to true the first time the room is described, so this almost does the same thing, except when recognizableInDark in dark is true and the description of a dark room is displayed, in which case visited is set to true (because the player character knows s/he's been to this room) but examined isn't (because the full room description won't have been displayed).
- recognizableInDark (true or nil) If this is set to true, then a room is set to both familiar and visited if a room description is shown when it's dark (typically, because the player character enters the room when it's dark). This allows game authors to distinguish between a room that's so dark the player character can't even tell where s/he is (recognizableInDark = nil, the default) and a room that's too dark to see much by but nevertheless recognizable (e.g. a dark cellar, which the player knows must be the cellar even though there's little light there).
- travelerLeaving(traveler, dest): this is invoked on the room when traveler is about to leave the room to go to dest.
- travelerEntering(traveler, origin): this is invoked on the room when traveler is about to enter it from another origin (another room)
- getDirection(conn) returns the direction in which one would have to travel from the room in order to travel via conn (i.e. the direction corresponding to the direction property on which conn is defined). For example, if frontDoor was assigned to the north property of a room called hall, hall.getDirection(frontDoor) would return northDir.
- getDirectionTo(dest) returns the direction in which one would have to travel from the room in order to travel to dest For example, if frontDoor was assigned to the north property of a room called hall and the front door led to a room called
drive, hall.getDirectionTo(drive) would return northDir. If dest is not a neighbouring room the method returns nil. - getConnectorTo(dest) returns the connector via which one would have to travel from the room in order to travel to dest For example, if frontDoor was assigned to the north property of a room called hall and the front door led to a room called
drive, hall.getConnectorTo(drive) would return frontDoor.
Note that some of these explanations involve concepts we haven't come to yet. Don't worry; they will be explained fully in their place when we come to them.
The Floor of a Room
The ground (or floor) is present virtually everywhere (except for rooms representing odd locations like the tops of trees or masts). The library defines a Floor class, and one instance of it, defaultGround, to represent the presence of the floor/ground in every room. A Floor is a combination of MultiLoc and Decoration that (by default) is added into every Room. Its main purpose is to facilitate the parser's ability to disambiguate items by their locations. Without it, if, say, there were two identical coins, one on a table and one directly in the room, the parser would have to ask "Which do you mean, the coin on the table or the coin?", which is unclear and fails to give the player an easy way of selecting the latter. Thanks to the presence of a defaultGround in every Room the parser can ask "Which do you mean, the coin on the table or the coin on the ground?" and the player can refer to "the coin on the ground" to disambiguate.
The defaultGround object present in every room also performs the secondary purpose of allowing players to refer to 'the ground' or 'the floor' which must be implicitly present in nearly every room, but its implementation is deliberately minimalistic to discouraage players from trying to interact with it. The library will translate PUT SOMETHING ON FLOOR to DROP SOMETHING, and X GROUND/FLOOR will of course work, but that's about it (everything else gets the standard decoration response 'The ground is not important')
If you want to define a custom Floor object for a particular location, or omit it altogether (e.g. for a room at the top of a tree), you can do so by overring the floorObj property, either to point to your custom Floor object, or to nil (in the case of a room without a floor). You should do this even if you implement your custom floor object as a Fixture in a single location rather than using the custom Floor class, but you might find it better to use the Floor class even for a custom floor that appears only in one room, since it's designed to facilitate the parser disambiguation just described. If you do decide to define your own Fixture, you'll need to copy most of the methods and properties of the Floor class onto it to make it work properly as a floor.
Closed Containers as Quasi-Rooms
Although all the above properties and methods have been described as belonging to Room, several of them are in fact defined on Thing, to allow for the possibility that the player character may at some point be inside a closed container and look around from inside it. In that case you can use the roomTitle, darkName and darkDesc properties on the containing Thing to determine what it should be called and how it should be described from the inside. These work in just the same way as they do for Room, as does the isIlluminated() method. You can also override the interiorDesc property to describe how the closed container looks from the inside.
When the actor is in object such as a Booth (or other enterable container), we normally show the enclosing room's interiorDesc provided the actor can see out to the enclosing Room (either because the Booth is open or because it's transparent). If the enclosing room can't be seen, we use the Booth's interiorDesc. For some Booth-type objects (such as a small garden shed, for instance) we may prefer to use the Booth's interiorDesc even when the actor couid see out through the open door, say. We can achieve this by overriding the Booth's useInteriorDesc to true. We could also do this on a Platform type object, which might be appropriate if the Platform represents a large area such as a stage.
Here's an example of how we might implement a shed in a garden as an enterable container with a ContainerDoor. We'll assume that the garden is part of a larger SenseRegion, but we don't want an actor to be able to see the other rooms (apart from the garden, that is) when the actor is inside the shed (this is probably more realistic):
backGarden: Room 'Back Garden'
"The garden has a neglected, overgrown air about it. It stretches from the back of the tall
house to the west to a flight of steps leading down to a wooden jetty to the east. A small
garden shed located against one border wall faces a small greenhouse backed against the fence
opposite. "
/* We'd need to define these other objects in a real game, of course. */
regions = [jettyRegion, outdoors]
west = jettySteps
down asExit(west)
/*
* Arrange it so that when the actor is inside the garden shed they can't see any other room
* in the SenseRegion (even when the shed is open). It makes sense that the shed door would
* offer only a limited view over the garden.
*/
canSeeOutTo(loc)
{
return !gActor.isIn(gardenShed);
}
/* But we should be able to see into the garden from anywhere else in its SenseRegion */
canSeeInFrom(loc) { return true; }
;
+ gardenShed: Fixture, Booth 'garden shed; small wooden'
/* Vary our description according to whehter we're being viewed from the outside and the inside */
"<<if gLocation == self.remapIn>><<remapIn.interiorDesc>><<else>><<exteriorDesc>><<end>> "
/* This is a custom property we've defined for this particular case; not a library property. */
exteriorDesc = "It's a small wooden shed. "
remapIn: SubComponent {
isEnterable = true
isOpenable = true
isOpen = nil
interiorDesc = "Unlike the Tardis, this small shed is no bigger on the inside than the
outside, meaning it's pretty cramped. Light streams in through a small window <<if
isOpen>> and the open door, through which you can see part of the garden and the
greenhouse opposite<<end>>. "
desc = interiorDesc
useInteriorDesc = true
allowReachOut(obj) { return nil; }
lockability = lockableWithKey
}
;
++ lawnmower: Heavy 'lawnmower; lawn; mower'
canPushTravel = true
specialDesc = "An ancient lawnmower rests on the ground <<location.objInName>>. "
sLoc(In)
;
++ Decoration 'small window'
"The window is quite dirty but you can see that it looks towards the house. "
isLit = true
sLoc(In)
lookThroughMsg = 'The window looks towards the house. '
decorationActions = inherited + [LookThrough]
;
++ ContainerDoor '(shed) door; sturdy wooden rusty;lock'
"It's a sturdy wooden door with a rusty lock. "
;
++ Component 'small window; (shed) exterior'
"The window on the side of the shed is no more than about nine inches square and faces the
house. "
/*
* Adjusting the vocabLikelihood of this window object according to whether the actor is inside or outside
* the shed is probably the easiest way of ensuring the parser chooses the right window object when
* both are in scope. Another approach might be to use filterResolveList(). It's best to avoid implementing
* both sides of the window as a MultiLoc in this situation, since this risks presenting the player
* with odd disambuguation messages.
*/
vocabLikelihood = (gActor.isIn(location) ? -10 : 10)
lookThroughMsg = 'The window is too dirty to see much through. '
disambigName = 'exterior shed window'
;
This example probably reaches the limit of how room-like an enterable container can be made to get. Note how almost everything to do with the shed has to be defined on or with reference to its remapIn property.
Regions
Regions in adv3Lite are simply a means of grouping Rooms together in any way you find useful (e.g. all downstairs rooms, all indoor rooms, all forest rooms, all riverside rooms, all outdoor rooms). You don't have to use Regions if you don't want to, but they are straightforward to use if you do. To include a Room in a Region, simply list that Region in the Room's region property and create a corresponding Region object, for example:
kitchen: Room 'Kitchen' 'kitchen'
"This kitchen is equipped much as you'd expect, with, for example, a sink
over by the window, a large table in the middle of the room, and an oven
over by the back door to the east, not far from the fridge. The other exits
are west to the hall, north to the dining-room and down to the cellar. "
regions = [downstairs]
;
downstairs: Region
;
Regions can themselves be included within other regions by setting their regions property. For example, to place the downstairs Region entirely within the indoors Region we could write:
downstairs: Region regions = [indoors] ; indoors: Region ;
It is also perfectly legal to define the regions property of Rooms in such a way that Regions end up overlapping.
The properties and methods of Region you may find useful include:
- regions A user-defined list of one or more regions that wholly contain this Region.
- isIn(other) Tests whether this Region is directy or indirectly contained in the other Region.
- isOrIsIn(other) Tests whether this Region either is the other Region or is directly or indirectly contained in the other Region.
- roomList A list of all the Rooms lying within this Region (note: this list is built by the library and should not be altered by user code)
- rooms A user-defined list of Rooms that are directly within this region. This can be used to define the rooms (or some of the rooms) that go to make up a Region. At the pre-initialization stage this list will be used in conjunction with the regions property of individual Rooms to build the roomList for each Region and update the regions list of each Room. For a fuller explanation see below.
- familiar (true or nil) Making a Region familiar has the effect of making every Room in the Region familiar. This in turn can be useful for enabling the player character to find his/her way around an area s/he already knows at the start of the game using the GO TO command.
- isFamiliar(prop) Delegates whether or not the Region is familiar to the prop property of the Region. If prop is not specified it defaults to &familiar. This is intended for use in games where the player character may change and different properties are used to check the knowledge of different actors.
- extraScopeItems A list of items that will be put into scope for every Room in the Region (even if they would not normally be in scope).
- travelerLeaving(traveler, dest): this is invoked on the region when traveler is about to leave the region to go to dest (a room).
- travelerEntering(traveler, origin): this is invoked on the region when traveler is about to enter it from another region, and specifically from the room given in the origin parameter.
- regionBeforeAction(): this is invoked on the region just before an action takes place in any room in the region.
- regionAfterAction(): this is invoked on the region just after an action takes place in any room in the region.
- regionBeforeTravel(traveler, connector): this is invoked on the region just before a traveler in any room in the region is about to travel via connector; note that this method is invoked after all other before travel notifications (to allow more specific ones to intervene first).
- regionAfterTravel(traveler, connector): this is invoked on the region after a traveler in any room in the region has traveled via connector; note that this method is invoked after all other after travel notifications (to allow more specific ones to react first).
- fastGoTo: if gameMain.fastGoTo is true, the setting of fastGoTo on individual regions will have no effect, since fast GoTo (GoTo without stopping for CONTINUE commands) will then be in effect globally. If gameMain.fastGoTo is nil, however, setting it to true on an individual region will allow fast GoTo travel within that Region. Note that if
fastGoTois set to true on a Region, the GOTO MODE command for any option other that GOTO MODE BRIEF will have no effect within that region, which some players may find confusing. - briefGoTo: if gameMain.briefGoTo is true, the setting of briefGoTo on individual regions will have no effect, since briefGoTo (GoTo without stopping for CONTINUE commands or displaying intervening room descriptions) will then be in effect globally. If gameMain.briefGoTo is nil, however, setting it to true on an individual region will allow fast GoTo travel (without intervening room descriptions) within that Region. Note that if
briefGoTois set to true on a Region, the GOTO MODE command will have no effect within that region, which some players may find confusing. - regionDaemon(): A method that is executed each turn the player character is located in this region. By default we call the region's doScript() method, which could be useful if the region is mixed in with an EventList class.
Some of the uses of Regions depend on features of the library we have not yet covered, and will need to be mentioned again when we come to them, but in summary the main uses to which Regions can be put include:
- Testing whether the player character (or some other object) is within a particular region as a condition of displaying atmospheric messages (e.g. about weather conditions or forest sounds), or for any other purpose related to a region (e.g. perhaps particular actions are allowed or disallowed in a particular region, or an NPC's response to a question depends on the region where the conversation takes place, or the effect of waving a magic wand varies from region to region or... well, you probably get the picture).
- Specifying the location of a MultiLoc (an object that can be in several rooms at once).
- Specifying the where condition of a Doer.
- Designating a region as familiar at the start of a game so that the player character can navigate it with the pcRouteFinder (i.e. using the GO TO command) without having to explore it first.
- Conditionally preventing travel between Regions or making things happen when travel between Regions occurs (using the travelerLeaving() and travelerEntering() methods).
A further word of explanation may be in order about travelerLeaving() and travelerEntering(). The first is called on all the regions the traveler is about to leave, and the second on all the regions the traveler is about to enter. Leaving a region means travelling from a room that is an that region to a room that is not. Conversely entering a region means traveling from a room that is not in that region to one that is. So, for example, if throneRoom is in regions A, B, C and D and corridor is in regions C, D, E and F, traveling from throneRoom to corridor would cause travelerLeaving() to be invoked on regions A and B (as well as throneRoom) and travelerEntering() to be invoked on Regions E and F (as well as corridor).
Note that all these notifications take place just before the travel is actually executed. If you want something to take place immediately after the traveler enters a region one way to do it would be to set a zero-length fuse in the travelerEntering() method, e.g.:
planeRegion: Region
travelerLeaving(traveler, dest) { "You're about to leave the plane. "; }
travelerEntering(traveler, origin)
{
"You're about to enter the plane. ";
new Fuse(self, &edesc, 0);
}
edesc = "You've just boarded the plane. "
;
One further point: several of the properties on which the Region mechanisms depend are set up by the library at the preinitialization stage. In particular the roomList property of a Region is built at PreInit stage. This means that the layout of regions cannot be changed during the course of a game. If you need a Region that changes during the course of your game you could try the DynamicRegion extension.
If a Region is defined with a rooms property containing a list of rooms, each of these rooms will have that Region added to its regions list, but the building of the Region's roomList will still proceed as before (removing duplicate entries in any case). This makes it safe to define the Rooms that go into a Region either by listing the Regions in a Room's regions property, or by listing the rooms if a Region's rooms property, or a mixture of both.
There is absolutely no need to define both the regions property of a Rooms and the rooms property of a Region to associate Rooms with Regions, but no harm will be done if you do. The purpose is simply to allow both methods of associating Rooms with Regions so that game authors can use whichever method they find most congenial, including a mixture of the two.
The rooms property of a Region can be specified via a template, thus:
downstairsRegion: Region [hall, kitchen, study, lounge] regions = [indoorRegion] ;
In this example, note how putting one Region inside another must still be done via the enclosed Region's regions property. If you have a larger Region that encloses smaller Regions you could also use the rooms property on the larger Region to list the smaller Regions that go to make it up. The point to bear in mind is that the regions property of X can be used to define the regions that X is in, while the rooms property can be used to define the Rooms (or Regions) that are in X, and that these provide alternative means for defining the same relationship.
